/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import http from '@ohos.net.http';
import Log from './log.js';

const {asyncAll, ensureTimestamp, Base64} = require('./utils');
const {MixPanelGroups} = require('./groups');
const {MixPanelPeople} = require('./people');

const DEFAULT_CONFIG = {
    test: false,
    debug: false,
    verbose: false,
    host: 'api.mixpanel.com',
    protocol: 'https',
    path: '',
};

var serialize = function (obj, prefix) {
    var str = [],
        p;
    for (p in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, p)) {
            var k = prefix ? prefix + '[' + p + ']' : p,
                v = obj[p];
            str.push((v !== null && typeof v === 'object') ?
            serialize(v, k) : encodeURIComponent(k) + '=' + encodeURIComponent(v));
        }
    }
    return str.join('&');
};


var createClient = function (token, config) {
    if (!token) {
        throw new Error('The Mixpanel Client needs a Mixpanel token: `init(token)`');
    }

    // mixpanel constants
    const MAX_BATCH_SIZE = 50;
    const TRACK_AGE_LIMIT = 60 * 60 * 24 * 5;

    const metrics = {
        token,
        config: { ...DEFAULT_CONFIG },
    };

    /**
     * sends an async GET or POST request to mixpanel
     * for batch processes data must be send in the body of a POST
     * @param {object} options
     * @param {string} options.endpoint
     * @param {object} options.data         the data to send in the request
     * @param {string} [options.method]     e.g. `get` or `post`, defaults to `get`
     * @param {function} callback           called on request completion or error
     */
    metrics.sendRequest = function (options, callback) {
        callback = callback || function () {
        };

        let content = Base64.encode(JSON.stringify(options.data));
        const endpoint = options.endpoint;
        const method = (options.method || 'GET').toUpperCase();
        let queryParams = {
            'ip': 0,
            'verbose': metrics.config.verbose ? 1 : 0
        };
        const key = metrics.config.key;
        const secret = metrics.config.secret;
        let requestOptions = {
            host: metrics.config.host,
            port: metrics.config.port,
            headers: {},
            method: method
        };

        if (method === 'POST') {
            content = 'data=' + content;
            requestOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded';
        } else if (method === 'GET') {
            queryParams.data = content;
        }

        // add auth params
        if (secret) {
            const encoded = Base64.encode(secret + ':');
            requestOptions.headers['Authorization'] = 'Basic ' + encoded;
        } else if (key) {
            queryParams.apiKey = key;
        } else if (endpoint === '/import') {
            throw new Error('The Mixpanel Client needs a Mixpanel API Secret when importing ' +
            'old events: `init(token, { secret: ... })`');
        }

        if (metrics.config.test) {
            queryParams.test = 1;
        }

        requestOptions.path = metrics.config.path + endpoint + '?' + serialize(queryParams);
        var httpCarrier = http.createHttp();
        var parsedUrl = 'https://' + requestOptions.host + requestOptions.path;
        var httpOptions = {
            method: requestOptions.method,
            header: requestOptions.headers,
        };

        if (method === 'POST') {
            httpOptions['extraData'] = content;
        }

        httpCarrier.request(parsedUrl, httpOptions, (error, data) => {
            if (error == null) {
                var e;
                if (metrics.config.verbose) {
                    try {
                        var result = JSON.parse(data.result);
                        if (result.status != 1) {
                            e = new Error('Mixpanel Server Error: ' + result.error);
                        }
                    }
                    catch (ex) {
                        e = new Error('Could not parse response from Mixpanel');
                    }
                }
                else {
                    Log.showInfo('data.result: ' + data.result);
                    e = (data.result !== '1') ? new Error('Mixpanel Server Error: ' + data) : undefined;
                }

                callback(e);
            } else {
                if (metrics.config.debug) {
                    Log.showInfo('Got Error: ' + e.message);
                }
                callback(error);
            }
        });
    };

    /**
     * Send an event to Mixpanel, using the specified endpoint (e.g., track/import)
     * @param {string} endpoint - API endpoint name
     * @param {string} event - event name
     * @param {object} properties - event properties
     * @param {Function} [callback] - callback for request completion/error
     */
    metrics.sendEventRequest = function (endpoint, event, properties, callback) {
        properties.token = metrics.token;
        properties.mpLib = 'node';

        var data = {
            event: event,
            properties: properties
        };

        if (metrics.config.debug) {
            Log.showInfo('Sending the following event to Mixpanel:\n', data);
        }

        metrics.sendRequest({ method: 'GET', endpoint: endpoint, data: data }, callback);
    };

    /**
     * breaks array into equal-sized chunks, with the last chunk being the remainder
     * @param {Array} arr
     * @param {number} size
     * @returns {Array}
     */
    var chunk = function (arr, size) {
        var chunks = [],
            i = 0,
            total = arr.length;

        while (i < total) {
            chunks.push(arr.slice(i, i += size));
        }
        return chunks;
    };

    /**
     * sends events in batches
     * @param {object}   options
     * @param {[{}]}     options.eventList                 array of event objects
     * @param {string}   options.endpoint                   e.g. `/track` or `/import`
     * @param {number}   [options.maxConcurrentRequests]  limits concurrent async requests over the network
     * @param {number}   [options.maxBatchSize]           limits number of events sent to mixpanel per request
     * @param {Function} [callback]                         callback receives array of errors if any
     *
     */
    var sendBatchRequests = function (options, callback) {
        var eventList = options.eventList,
            endpoint = options.endpoint,
            maxBatchSize = options.maxBatchSize ? Math.min(MAX_BATCH_SIZE, options.maxBatchSize) : MAX_BATCH_SIZE,

            /**
             * to maintain original intention of maxBatchSize; if maxBatchSize is greater than 50,
             * we assume the user is trying to set maxConcurrentRequests
             */
            maxConcurrentRequests = options.maxConcurrentRequests ||
            (options.maxBatchSize > MAX_BATCH_SIZE && Math.ceil(options.maxBatchSize / MAX_BATCH_SIZE)),
            eventBatches = chunk(eventList, maxBatchSize),
            requestBatches = maxConcurrentRequests ? chunk(eventBatches, maxConcurrentRequests) : [eventBatches],
            totalEventBatches = eventBatches.length,
            totalRequestBatches = requestBatches.length;

        /**
         * sends a batch of events to mixpanel through http api
         * @param {Array} batch
         * @param {Function} cb
         */
        function sendEventBatch(batch, cb) {
            if (batch.length > 0) {
                batch = batch.map(function (event) {

                    if (endpoint === '/import' || event.properties.time) {
                        // usually there will be a time property, but not required for `/track` endpoint
                        event.properties.time = ensureTimestamp(event.properties.time);
                    }
                    event.properties.token = event.properties.token || metrics.token;
                    return event;
                });

                // must be a POST
                metrics.sendRequest({ method: 'POST', endpoint: endpoint, data: batch }, cb);
            }
        }

        /**
         * Asynchronously sends batches of requests
         * @param {number} index
         */
        function sendNextRequestBatch(index) {
            var requestBatch = requestBatches[index],
                cb = function (errors, results) {
                    index += 1;
                    if (index === totalRequestBatches) {
                        callback && callback(errors, results);
                    } else {
                        sendNextRequestBatch(index);
                    }
                };

            asyncAll(requestBatch, sendEventBatch, cb);
        }

        // init recursive function
        sendNextRequestBatch(0);

        if (metrics.config.debug) {
            Log.showInfo(
                'Sending ' + eventList.length + ' events to Mixpanel in ' +
                totalEventBatches + ' batches of events and ' +
                totalRequestBatches + ' batches of requests'
            );
        }
    };

    /**
     * track(event, properties, callback)
     * ---
     * this function sends an event to mixpanel.
     *
     * event:string                    the event name
     * properties:object               additional event properties to send
     * callback:function(err:Error)    callback is called when the request is
     *                                 finished or an error occurs
     */
    metrics.track = function (event, properties, callback) {
        if (!properties || typeof properties === 'function') {
            callback = properties;
            properties = {};
        }

        // time is optional for `track` but must be less than 5 days old if set
        if (properties.time) {
            properties.time = ensureTimestamp(properties.time);
            if (properties.time < Date.now() / 1000 - TRACK_AGE_LIMIT) {
                throw new Error('`track` not allowed for event more than 5 days old; use `mixpanel.import()`');
            }
        }

        metrics.sendEventRequest('/track', event, properties, callback);
    };

    /**
     * send a batch of events to mixpanel `track` endpoint: this should only be used if events are less than 5 days old
     * @param {Array}    eventList                         array of event objects to track
     * @param {object}   [options]
     * @param {number}   [options.maxConcurrentRequests]  number of concurrent http requests that can be made to mixpanel
     * @param {number}   [options.maxBatchSize]           number of events that can be sent to mixpanel per request
     * @param {Function} [callback]                         callback receives array of errors if any
     */
    metrics.trackBatch = function (eventList, options, callback) {
        options = options || {};
        if (typeof options === 'function') {
            callback = options;
            options = {};
        }
        var batchOptions = {
            eventList: eventList,
            endpoint: '/track',
            maxConcurrentRequests: options.maxConcurrentRequests,
            maxBatchSize: options.maxBatchSize
        };

        sendBatchRequests(batchOptions, callback);
    };

    /**
     *
     * import(event, time, properties, callback)
     * ---
     * This function sends an event to mixpanel using the import
     * endpoint.  The time argument should be either a Date or Number,
     * and should signify the time the event occurred.
     *
     * It is highly recommended that you specify the distinctId
     * property for each event you import, otherwise the events will be
     * tied to the IP address of the sending machine.
     * For more information look at:
     * https://mixpanel.com/docs/api-documentation/importing-events-older-than-31-days
     * event:string                    the event name
     * time:date|number                the time of the event
     * properties:object               additional event properties to send
     * callback:function(err:Error)    callback is called when the request is
     *                                 finished or an error occurs
     */
    metrics.import = function (event, time, properties, callback) {
        if (!properties || typeof properties === 'function') {
            callback = properties;
            properties = {};
        }

        properties.time = ensureTimestamp(time);

        metrics.sendEventRequest('/import', event, properties, callback);
    };

    /**
     * importBatch(eventList, options, callback)
     * ---
     * This function sends a list of events to mixpanel using the import
     * endpoint. The format of the event array should be:
     *
     * [
     *     {
     *         'event': 'event name',
     *         'properties': {
     *             'time': new Date(), // Number or Date; required for each event
     *             'key': 'val',
     *             ...
     *         }
     *     },
     *     {
     *         'event': 'event name',
     *         'properties': {
     *             'time': new Date()  // Number or Date; required for each event
     *         }
     *     },
     *     ...
     * ]
     *
     * See import() for further information about the import endpoint.
     *
     * Options:
     *     maxBatchSize: the maximum number of events to be transmitted over
     *                     the network simultaneously. useful for capping bandwidth
     *                     usage.
     *     maxConcurrentRequests: the maximum number of concurrent http requests that
     *                     can be made to mixpanel; also useful for capping bandwidth.
     *
     * N.B.: the Mixpanel API only accepts 50 events per request, so regardless
     * of maxBatchSize, larger lists of events will be chunked further into
     * groups of 50.
     *
     * eventList:array                    list of event names and properties
     * options:object                      optional batch configuration
     * callback:function(errorList:array) callback is called when the request is
     *                                     finished or an error occurs
     */
    metrics.importBatch = function (eventList, options, callback) {
        var batchOptions;

        if (typeof (options) === 'function' || !options) {
            callback = options;
            options = {};
        }
        batchOptions = {
            eventList: eventList,
            endpoint: '/import',
            maxConcurrentRequests: options.maxConcurrentRequests,
            maxBatchSize: options.maxBatchSize
        };
        sendBatchRequests(batchOptions, callback);
    };

    /**
     * alias(distinctId, alias)
     * ---
     * This function creates an alias for distinctId
     *
     * For more information look at:
     * https://mixpanel.com/docs/integration-libraries/using-mixpanel-alias
     *
     * distinctId:string              the current identifier
     * alias:string                    the future alias
     */
    metrics.alias = function (distinctId, alias, callback) {
        var properties = {
            distinctId: distinctId,
            alias: alias
        };

        metrics.track('$create_alias', properties, callback);
    };

    metrics.groups = new MixPanelGroups(metrics);
    metrics.people = new MixPanelPeople(metrics);

    /**
     *
     * setConfig(config)
     * ---
     * Modifies the mixpanel config
     *
     * config:object       an object with properties to override in the
     *                     mixpanel client config
     */
    metrics.setConfig = function (config) {
        Object.assign(metrics.config, config);
        if (config.host) {
            // Split host into host and port
            const [host, port] = config.host.split(':');
            metrics.config.host = host;
            if (port) {
                metrics.config.port = Number(port);
            }
        }
    };

    if (config) {
        metrics.setConfig(config);
    }

    return metrics;
};

var mixpanel = { init: createClient, 'MixPanelGroups': MixPanelGroups, 'MixPanelPeople': MixPanelPeople };

export default mixpanel;
